import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.zip.CRC32;

/**
 * This class represents an EthernetFrame.
 * 
 * @author Jonathan Jung, Timothy Hong, Ronen Adam
 * 
 *         | Preamble | Start | Dest Addr | Source Addr | Type | Payload | CRC | <BR>
 *         </BR> | 7 bytes | 1 byte | 6 bytes | 6 bytes | 2 bytes | 46 - 1500
 *         bytes | 4 bytes |
 * 
 */
public class EthernetFrame {
    private byte[] frame;
    private byte[] des;
    private byte[] src;
    private byte[] type;
    private byte[] payload;
    private byte[] crc;

    private final int BYTE_OFFSET = 128;
    private final int MAX_SINGLE_DIGIT = -112;
    private final int FF_AS_BYTE = 127;

    // constructor for disassembling Ethernet frames given a byte array
    public EthernetFrame(byte[] data) throws InstantiationException {
        // Minimum length of a valid frame is 72 bytes. (subtracted the length
        // of preamble to get 64)
        // Maximum length of a valid frame is 1526 bytes.
        if (data.length < 72 || data.length > 1526)
            throw new InstantiationException(
                    "Frame not built: Invalid frame length.");

        frame = data;
        des = getDestinationAddress();
        src = getSourceAddress();
        type = getFrameType();
        payload = getFramePayload();
        crc = getFrameCRC();

        if (!isValidFrame()) {
            System.out.print("\nCheck both CRC's: " + hexify(crc) + " vs ");
            System.out.println(hexify(generateCRC()));
            throw new InstantiationException("Frame not built: Invalid CRC.");
        }
    }

    // constructor for assembling Ethernet frames given components
    public EthernetFrame(MacAddress des, MacAddress src, byte[] type,
            byte[] payload) {
        this.des = des.toByteArray();
        this.src = src.toByteArray();
        this.type = type;
        this.payload = payload;
        crc = generateCRC();
    }

    public byte[] toByte() {
        // if the preamble is needed

        // 26 for the sum of the sizes of preamble[8], source[6],
        // destination[6], type[2], and crc[4]
        byte[] ethernetOut = new byte[26 + payload.length];
        byte[] preamble = { 42, 42, 42, 42, 42, 42, 42, 43 };

        // System.arracopy is used to simply copy over multiple byte arrays to
        // on single byte array
        System.arraycopy(preamble, 0, ethernetOut, 0, 8);
        System.arraycopy(des, 0, ethernetOut, 8, 6);
        System.arraycopy(src, 0, ethernetOut, 14, 6);
        System.arraycopy(type, 0, ethernetOut, 20, 2);
        System.arraycopy(payload, 0, ethernetOut, 22, payload.length);
        System.arraycopy(crc, 0, ethernetOut, 22 + payload.length, 4);

        // if the preamble is not needed
        // //18 for the sum of the sizes of source[6], destination[6], type[2],
        // and crc[4]
        // byte [] ethernetOut = new byte [18 + payload.length];
        //
        //
        // //System.arracopy is used to simply copy over multiple byte arrays to
        // on single byte array
        //
        // System.arraycopy(des, 0, ethernetOut, 0, 6);
        // System.arraycopy(src, 0, ethernetOut, 6, 6);
        // System.arraycopy(type, 0, ethernetOut, 12, 2);
        // System.arraycopy(payload, 0, ethernetOut, 14, payload.length);
        // System.arraycopy(crc, 0, ethernetOut, 14 + payload.length, 4);

        return ethernetOut;
    }

    private byte[] getDestinationAddress() {
        // Bytes 8 - 14 of the frame contain the destination address.
        byte[] destinationAddress = new byte[6];

        // Read bytes.
        for (int i = 8; i < 14; i++)
            destinationAddress[i - 8] = frame[i];

        return destinationAddress;
    }

    private byte[] getSourceAddress() {
        // Bytes 16 - 22 of the frame contain the source address.
        byte[] sourceAddress = new byte[6];

        // Read bytes.
        for (int i = 14; i < 20; i++)
            sourceAddress[i - 14] = frame[i];

        return sourceAddress;
    }

    private byte[] getFrameType() {
        // Bytes 23 - 24 of the frame contain the frame type.
        byte[] frameType = new byte[2];

        // Read bytes.
        for (int i = 20; i < 22; i++)
            frameType[i - 20] = frame[i];

        return frameType;
    }

    public byte[] getFramePayload() {
        // Bytes 25 - 1525 of the frame contain the frame's payload.
        byte[] framePayload = new byte[frame.length - 4 - 22];

        // Read bytes.
        for (int i = 22; i < frame.length - 4; i++)
            framePayload[i - 22] = frame[i];

        return framePayload;
    }

    public byte[] getFrameCRC() {
        // The last 4 bytes of the frame contain the CRC data.
        byte[] frameCRC = new byte[4];

        // Read bytes.
        for (int i = frame.length - 4; i < frame.length; i++)
            frameCRC[i - (frame.length - 4)] = frame[i];

        return frameCRC;
    }

    /**
     * Assembles an ethernet frame.
     */

    /**
     * Disassembles an ethernet frame.
     */
    // TODO: take in byte arrray and convert to bid endian and most significant
    // bit order
    private void disassembleFrame() {

    }

    /**
     * Validates the integrity of the ethernet frame using the CRC data.
     */
    private boolean isValidFrame() {
        // temporary byte array contains a CRC value from generateCRC() to test
        // against the value of the incoming value
        byte[] temp = new byte[4];
        temp = generateCRC();

        if (Arrays.equals(temp, getFrameCRC()))
            return true;
        else
            return false;
    }

    /**
     * Creates a new CRC to be put into the Ethernet Frame.
     */
    public byte[] generateCRC() {
        // 14 for the sum of the sizes of source, destination, and type
        byte[] partialFrame = new byte[14 + this.payload.length];
        byte[] temp;
        byte[] generated = new byte[4];

        /*
         * Create an array of the part of the Ethernet Frame used for creating
         * the CRC Components used are destination, source, type, and payload
         */

        System.arraycopy(des, 0, partialFrame, 0, 6);
        System.arraycopy(src, 0, partialFrame, 6, 6);
        System.arraycopy(type, 0, partialFrame, 12, 2);
        System.arraycopy(payload, 0, partialFrame, 14, payload.length);

        // With the newly created CRC, the long is converted to a byte array
        CRC32 newChecksum = new CRC32();
        newChecksum.update(partialFrame);
        ByteBuffer bb = ByteBuffer.allocate(8);
        temp = bb.putLong(newChecksum.getValue()).array();

        // Since Longs are 8 bytes, the first four bytes, which seem to be
        // padded with zeros, are removed
        System.arraycopy(temp, 4, generated, 0, 4);

        return generated;
    }

    /**
     * Converts byte array to hex
     */
    public String hexify(byte[] n) {
        StringBuilder temp = new StringBuilder();

        for (int i = 0; i < n.length; i++) {
            // Pad a leading '0' to single-digit values of the pair.
            if (n[i] < MAX_SINGLE_DIGIT)
                temp.append("0");

            /*
             * Adjust the value of the pair by 128 so the range is 0 to 255. and
             * represent the pair's value as a hex string.
             */
            temp.append(Integer.toHexString((n[i] + BYTE_OFFSET)));
        }

        String result = temp.toString();

        return result;
    }

    /**
     * toString overridden to return addresses and the size of the payload
     */
    @Override
    public String toString() {
        MacAddress destination = new MacAddress(des);
        MacAddress source = new MacAddress(src);

        return "Des MAC " + destination.toString() + "\nSrc MAC "
                + source.toString() + "\nPaload length " + payload.length
                + "\nCRC: " + hexify(crc);
    }

    public static void main(String[] args) {
        ByteBuffer z = ByteBuffer.allocate(72);
        byte[] test = new byte[72];

        // Populate the test ethernet frame.
        // Preamble.
        for (int i = 0; i < 7; i++) {
            test[i] = 42;
        }

        // Start of frame.
        test[7] = 43;

        // Destination address.
        for (int i = 8; i < 14; i++) {
            test[i] = 15;
        }

        // Source address.
        for (int i = 14; i < 20; i++) {
            test[i] = 20;
        }

        // Type / length.
        test[20] = -128;
        test[21] = -128;

        // Payload / data.
        for (int i = 22; i < 68; i++) {
            test[i] = -101;
        }

        // CRC.
        test[68] = 103;
        test[69] = -128;
        test[70] = -8;
        test[71] = -20;

        z.put(test);

        EthernetFrame eframe;
        try {
            eframe = new EthernetFrame(z.array());

            System.out.println(eframe.toString());

            byte[] partialFrame = new byte[4];
            partialFrame = eframe.generateCRC();

            EthernetFrame eframe3 = new EthernetFrame(eframe.toByte());

        } catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}